Add generic OIDC auth provider with native Microsoft Entra support - #192
Open
antosubash wants to merge 5 commits into
Open
Add generic OIDC auth provider with native Microsoft Entra support#192antosubash wants to merge 5 commits into
antosubash wants to merge 5 commits into
Conversation
Introduce a new optional `oidc` auth-provider module that authenticates against any OpenID Connect provider via its discovery document, with a first-class `entra` preset for Microsoft Entra ID (Azure AD). Configuration is preset-driven: pick SM_OIDC_PROVIDER plus the few required secrets and the preset fills claim defaults and derives the discovery URL. The well-known document supplies the authorize/token/JWKS/end-session endpoints and issuer, so no per-provider URLs are hardcoded. Entra specifics baked into the preset: validate the id_token in the browser callback (Entra's Graph access tokens aren't app-validatable), key the user cache on the stable `oid` claim, and default the roles claim to `roles`. The JWKS validator also accepts RSA keys that omit `alg`, as Entra's keys do. Supports both browser-interactive login and bearer-token validation. Shipped as an optional swap-in (not active in host/ by default) like the keycloak module; keycloak is left untouched. Generalize test_app_state_has_sm_services to keep a single auth provider now that three are installed in the workspace. https://claude.ai/code/session_01SbQuMY1b1tEoKkYTs1eb1C
Deploying simple-module-python with
|
| Latest commit: |
d9fbf04
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a794ce79.simple-module-python.pages.dev |
| Branch Preview URL: | https://claude-microsoft-entra-nativ.simple-module-python.pages.dev |
antosubash
marked this pull request as ready for review
June 5, 2026 09:55
# Conflicts: # pyproject.toml
…route
Three issues found while testing the OIDC provider end-to-end:
- Security: the login nonce was stored in the session then discarded at
callback without ever being compared to the id_token `nonce` claim, so a
separately-obtained id_token could be replayed/injected. Now validated
(OIDC Core 3.1.3.7 §11).
- Correctness: `_upsert_user_cache` opened its own session and only flushed,
so the subject->UUID row was rolled back on close and never persisted —
every login minted a fresh framework id. Now commits explicitly.
- Routing: the `oidc_login`/`oidc_callback` route function names collide with
keycloak's, so `url_for("oidc_callback")` could resolve to keycloak's
endpoint and send the IdP the wrong redirect_uri. Routes now carry unique
names (`oidc_auth_login` / `oidc_auth_callback`).
Adds an end-to-end login->callback flow test (real RS256 id_token validated
against an injected JWKS key) covering the happy path, stable-id reuse across
logins, and rejection of nonce/state/audience mismatches. Also adds the
project's standard inline `unsupported-base` suppression to the new model.
Claude-Session: https://claude.ai/code/session_012kKthZeQPYEUWRL4jygquY
ty 0.0.52 (released 2026-06-23) tightened its diagnostics, turning the lint gate red on main independently of any single PR: - It now requires the project's inline `# ty: ignore[unsupported-base]` SQLModel suppression (carried by 24 model classes) on keycloak's `KeycloakUserCache`, which was missing it. - It flags four `# ty: ignore[invalid-assignment]` directives as unused, because `invalid-assignment` is already globally ignored in pyproject. Add the missing keycloak suppression and drop the four redundant inline directives. No behaviour change. Claude-Session: https://claude.ai/code/session_012kKthZeQPYEUWRL4jygquY
antosubash
added a commit
that referenced
this pull request
Jun 25, 2026
…ntial guards & admin UI (#219) * docs: design for external (SSO) users in the users module Marks IdP-provisioned users external with a genuinely null password, guards all password-credential paths, surfaces the marking in the admin UI, and keeps normal role assignment. Independent of PR #192. Claude-Session: https://claude.ai/code/session_012kKthZeQPYEUWRL4jygquY * feat(users): provision external (SSO) users with null password Users created via OAuth/OIDC login (Microsoft/Entra, Google, GitHub, generic OIDC) are now first-class rows in users_user, marked external with a truly NULL password instead of a random one — so they sign in only through their IdP and admins manage their roles like any other user. Backend - models/user.py: hashed_password is now nullable; add is_external (server_default false). Migration 92965b00f105 makes the column nullable + adds the marker via batch_alter_table (SQLite rebuild recreates the lower(email) functional index). Forks off the users head (873ca2015033), not the keycloak branch, keeping the users migration line independent of the optional keycloak provider. - oauth/api.py: flag the request before find-or-create so the manager can mark only *newly provisioned* OAuth users. Logins that link to an existing password account are untouched (on_after_register won't fire) — link-by-email behaviour is preserved. - manager.py: on_after_register nulls the password + sets is_external for OAuth-provisioned users; authenticate() and forgot_password() refuse external users (no local password); generate_reset_password_token raises ExternalUserNoPasswordError instead of hashing None. - admin reset-password-link returns 409 for external users. - expose is_external in UserRead / UserListItem + admin list query. Frontend - list + detail surface an "External · SSO" / SSO badge; the detail page hides the password-reset action for external users and explains why. Tests - test_external_users.py: provisioning (new vs linked), credential guards (login, reset-link, forgot-password no-op), role assignment, and admin-list visibility. Full users suite: 303 passed. No default role is assigned; scope covers any OAuth/OIDC-provisioned user. The standalone oidc module (PR #192) remains separate for the bearer/stateless use case. Claude-Session: https://claude.ai/code/session_012kKthZeQPYEUWRL4jygquY * fix(users): block bearer-token login for external (SSO) users Code-review (round 1) finding: the bearer-token login path (POST /api/users/auth/token) verified the password directly instead of going through the guarded UserManager.authenticate, so an external user (hashed_password is None) hit verify_and_update(pw, None) -> TypeError -> 500, which also leaked account type via timing/error (missing user got a clean 401 after a dummy hash). Treat null-password users like a missing user: run the dummy hash and return 401. Also align forgot_password's no-op guard to (is_external or hashed_password is None) to match generate_reset_password_token, add a regression test for the bearer path, and note the downgrade constraint. Claude-Session: https://claude.ai/code/session_012kKthZeQPYEUWRL4jygquY * fix: resolve ty 0.0.52 diagnostics (repo-wide lint unblock) ty 0.0.52 newly flags previously-valid suppressions. Remove four now-unused '# ty: ignore[invalid-assignment]' directives (framework/core/tests/*, users/backend.py) and add '# ty: ignore[unsupported-base]' to keycloak's SQLModel table class, matching the users User model. Comment-only; no behavior change. Unblocks 'make lint' / CI typecheck, which fails repo-wide otherwise. Claude-Session: https://claude.ai/code/session_012kKthZeQPYEUWRL4jygquY
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Adds native Microsoft Entra ID (Azure AD) support. Rather than clone the
keycloakmodule into a third near-identical provider, this introduces one discovery-driven generic OIDC auth-provider module (modules/oidc/) and ships Entra as a first-class preset — which also covers Auth0, Okta, Zitadel, Authentik, Keycloak, and any OIDC-compliant IdP for free.The framework already has a pluggable auth-provider slot (
_is_auth_provider = True+app.state.auth.auth_provider, enforced single bySM020/SM021), andmodules/keycloak/was already ~90% provider-agnostic. This module generalizes that pattern.How it works
.well-known/openid-configuration), which supplies the authorize/token/JWKS/end-session endpoints and issuer — no per-provider URL templating.SM_OIDC_PROVIDER+ a few secrets; the preset fills claim defaults and derives the discovery URL.host/by default (like keycloak). Keycloak is left untouched.Native Entra
Entra specifics baked into the preset:
oidclaim (notsub).roles_claim_pathtoroles(Entra app roles).alg, as Entra's keys do (keycloak's stricter filter would have dropped them).Files
New module under
modules/oidc/mirroring the keycloak structure:settings.py(+presets.py),discovery.py,client.py,jwks.py,provider.py,models.py,module.py,endpoints/{api,views}.py,pages/{Login,LoggedOut}.tsx,locales/en.json, README, and tests. Workspace wiring added to rootpyproject.toml(ty paths + pytest testpaths;modules/*glob already covers membership). One existing test (test_app_state_has_sm_services) generalized to keep a single auth provider now that three are installed in the dev workspace.Verification
uv run pytest— full suite green (1319 passed, 1 skipped); new module has 33 tests covering presets, provider claim-mapping, discovery parsing, client URL building, and JWKS validation (incl. the no-algEntra case).make lint— ruff format/check,ty, biome, per-moduletsc, 300-line cap, metadata/readme/hardcoded-string checks all pass.make doctorreportsSM020in the dev workspace only because users + keycloak + oidc are all installed at once; it is non-fatal in dev and unchanged in nature from the pre-existing users + keycloak situation. Withoidcnot in host deps, the running host's active provider set is unchanged.https://claude.ai/code/session_01SbQuMY1b1tEoKkYTs1eb1C
Generated by Claude Code